package com.zlyx.easy.swagger.plugins;

import static com.google.common.collect.Sets.newHashSet;
import static springfox.documentation.schema.ResolvedTypes.resolvedTypeSignature;
import static springfox.documentation.swagger.annotations.Annotations.resolvedTypeFromOperation;
import static springfox.documentation.swagger.common.SwaggerPluginSupport.pluginDoesApply;

import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.zlyx.easy.core.collections.Lists;
import com.zlyx.easy.core.loggers.Logger;
import com.zlyx.easy.swagger.annotations.SpringMapping;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationModelsProviderPlugin;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)
public class EasySwaggerOperationModelsProvider implements OperationModelsProviderPlugin {

	private final TypeResolver typeResolver;

	@Autowired
	public EasySwaggerOperationModelsProvider(TypeResolver typeResolver) {
		this.typeResolver = typeResolver;
	}

	@Override
	public void apply(RequestMappingContext context) {
		collectFromApiOperation(context);
		collectApiResponses(context);
	}

	@Override
	public boolean supports(DocumentationType delimiter) {
		return pluginDoesApply(delimiter);
	}

	private void collectFromApiOperation(RequestMappingContext context) {
		ResolvedType returnType = context.getReturnType();
		returnType = context.alternateFor(returnType);
		Optional<ResolvedType> returnParameter = context.findAnnotation(ApiOperation.class)
				.transform(resolvedTypeFromOperation(typeResolver, returnType));
		if (returnParameter.isPresent() && returnParameter.get() != returnType) {
			Logger.debug("Adding return parameter of type {}",
					resolvedTypeSignature(returnParameter.get()).or("<null>"));
			context.operationModelsBuilder().addReturn(returnParameter.get());
		}
	}

	private void collectApiResponses(RequestMappingContext context) {
		List<ApiResponses> allApiResponses = context.findAnnotations(ApiResponses.class);
		Logger.debug("Reading parameters models for handlerMethod |{}|", context.getName());
		Set<ResolvedType> seenTypes = newHashSet();
		List<ResolvedType> modelTypes = collectApiResponse(context);
		for (ApiResponses apiResponses : allApiResponses) {
			modelTypes.addAll(toResolvedTypes(context).apply(apiResponses));
			for (ResolvedType modelType : modelTypes) {
				if (!seenTypes.contains(modelType)) {
					seenTypes.add(modelType);
					context.operationModelsBuilder().addReturn(modelType);
				}
			}
		}
	}

	private Function<ApiResponses, List<ResolvedType>> toResolvedTypes(final RequestMappingContext context) {
		return new Function<ApiResponses, List<ResolvedType>>() {
			@Override
			public List<ResolvedType> apply(ApiResponses input) {
				List<ResolvedType> resolvedTypes = Lists.newList();
				for (ApiResponse response : input.value()) {
					ResolvedType modelType = context.alternateFor(typeResolver.resolve(response.response()));
					Logger.debug("Adding input parameter of type {}", resolvedTypeSignature(modelType).or("<null>"));
					resolvedTypes.add(modelType);
				}
				return resolvedTypes;
			}
		};
	}

	private Function<ApiResponse, ResolvedType> toResolvedType(final RequestMappingContext context) {
		return new Function<ApiResponse, ResolvedType>() {
			@Override
			public ResolvedType apply(ApiResponse response) {
				ResolvedType modelType = context.alternateFor(typeResolver.resolve(response.response()));
				Logger.debug("Adding input parameter of type {}", resolvedTypeSignature(modelType).or("<null>"));
				return modelType;
			}
		};
	}

	private List<ResolvedType> collectApiResponse(RequestMappingContext context) {
		List<ResolvedType> resolvedTypes = Lists.newList();
		Optional<SpringMapping> annotation = context.findAnnotation(SpringMapping.class);
		if (annotation.isPresent()) {
			resolvedTypes.add(context.alternateFor(typeResolver.resolve(annotation.get().response())));
			for (ApiResponse response : annotation.get().apiResponses()) {
				resolvedTypes.add(toResolvedType(context).apply(response));
			}
		}
		return resolvedTypes;
	}

}
